// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/cert_net/cert_net_fetcher_impl.h"

#include <tuple>
#include <utility>

#include "base/callback_helpers.h"
#include "base/containers/linked_list.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/numerics/safe_math.h"
#include "base/stl_util.h"
#include "base/timer/timer.h"
#include "net/base/load_flags.h"
#include "net/url_request/redirect_info.h"
#include "net/url_request/url_request_context.h"

// TODO(eroman): Add support for POST parameters.
// TODO(eroman): Add controls for bypassing the cache.
// TODO(eroman): Add a maximum number of in-flight jobs/requests.
// TODO(eroman): Add NetLog integration.

namespace net {

namespace {

    // The size of the buffer used for reading the response body of the URLRequest.
    const int kReadBufferSizeInBytes = 4096;

    // The maximum size in bytes for the response body when fetching a CRL.
    const int kMaxResponseSizeInBytesForCrl = 5 * 1024 * 1024;

    // The maximum size in bytes for the response body when fetching an AIA URL
    // (caIssuers/OCSP).
    const int kMaxResponseSizeInBytesForAia = 64 * 1024;

    // The default timeout in seconds for fetch requests.
    const int kTimeoutSeconds = 15;

    // Policy for which URLs are allowed to be fetched. This is called both for the
    // initial URL and for each redirect. Returns OK on success or a net error
    // code on failure.
    Error CanFetchUrl(const GURL& url)
    {
        if (!url.SchemeIs("http"))
            return ERR_DISALLOWED_URL_SCHEME;
        return OK;
    }

    base::TimeDelta GetTimeout(int timeout_milliseconds)
    {
        if (timeout_milliseconds == CertNetFetcher::DEFAULT)
            return base::TimeDelta::FromSeconds(kTimeoutSeconds);
        return base::TimeDelta::FromMilliseconds(timeout_milliseconds);
    }

    size_t GetMaxResponseBytes(int max_response_bytes,
        size_t default_max_response_bytes)
    {
        if (max_response_bytes == CertNetFetcher::DEFAULT)
            return default_max_response_bytes;

        // Ensure that the specified limit is not negative, and cannot result in an
        // overflow while reading.
        base::CheckedNumeric<size_t> check(max_response_bytes);
        check += kReadBufferSizeInBytes;
        DCHECK(check.IsValid());

        return max_response_bytes;
    }

    enum HttpMethod {
        HTTP_METHOD_GET,
        HTTP_METHOD_POST,
    };

} // namespace

// CertNetFetcherImpl::RequestImpl tracks an outstanding call to Fetch().
class CertNetFetcherImpl::RequestImpl : public CertNetFetcher::Request,
                                        public base::LinkNode<RequestImpl> {
public:
    RequestImpl(Job* job, const FetchCallback& callback)
        : callback_(callback)
        , job_(job)
    {
        DCHECK(!callback.is_null());
    }

    // Deletion cancels the outstanding request.
    ~RequestImpl() override;

    void OnJobCancelled(Job* job)
    {
        DCHECK_EQ(job_, job);
        job_ = nullptr;
        callback_.Reset();
    }

    void OnJobCompleted(Job* job,
        Error error,
        const std::vector<uint8_t>& response_body)
    {
        DCHECK_EQ(job_, job);
        job_ = nullptr;
        base::ResetAndReturn(&callback_).Run(error, response_body);
    }

private:
    // The callback to invoke when the request has completed.
    FetchCallback callback_;

    // A non-owned pointer to the job that is executing the request.
    Job* job_;

private:
    DISALLOW_COPY_AND_ASSIGN(RequestImpl);
};

struct CertNetFetcherImpl::RequestParams {
    RequestParams();

    bool operator<(const RequestParams& other) const;

    GURL url;
    HttpMethod http_method;
    size_t max_response_bytes;

    // If set to a value <= 0 then means "no timeout".
    base::TimeDelta timeout;

    // IMPORTANT: When adding fields to this structure, update operator<().

private:
    DISALLOW_COPY_AND_ASSIGN(RequestParams);
};

CertNetFetcherImpl::RequestParams::RequestParams()
    : http_method(HTTP_METHOD_GET)
    , max_response_bytes(0)
{
}

bool CertNetFetcherImpl::RequestParams::operator<(
    const RequestParams& other) const
{
    return std::tie(url, http_method, max_response_bytes, timeout) < std::tie(other.url, other.http_method, other.max_response_bytes,
               other.timeout);
}

// CertNetFetcherImpl::Job tracks an outstanding URLRequest as well as all of
// the pending requests for it.
class CertNetFetcherImpl::Job : public URLRequest::Delegate {
public:
    Job(std::unique_ptr<RequestParams> request_params,
        CertNetFetcherImpl* parent);
    ~Job() override;

    // Cancels the job and all requests attached to it. No callbacks will be
    // invoked following cancellation.
    void Cancel();

    const RequestParams& request_params() const { return *request_params_; }

    // Create a request and attaches it to the job. When the job completes it will
    // notify the request of completion through OnJobCompleted. Note that the Job
    // does NOT own the request.
    std::unique_ptr<Request> CreateRequest(const FetchCallback& callback);

    // Removes |request| from the job.
    void DetachRequest(RequestImpl* request);

    // Creates and starts a URLRequest for the job. After the request has
    // completed, OnJobCompleted() will be invoked and all the registered requests
    // notified of completion.
    void StartURLRequest(URLRequestContext* context);

private:
    // The pointers in RequestList are not owned by the Job.
    using RequestList = base::LinkedList<RequestImpl>;

    // Implementation of URLRequest::Delegate
    void OnReceivedRedirect(URLRequest* request,
        const RedirectInfo& redirect_info,
        bool* defer_redirect) override;
    void OnResponseStarted(URLRequest* request) override;
    void OnReadCompleted(URLRequest* request, int bytes_read) override;

    // Clears the URLRequest and timer. Helper for doing work common to
    // cancellation and job completion.
    void Stop();

    // Reads as much data as available from |request|.
    void ReadBody(URLRequest* request);

    // Helper to copy the partial bytes read from the read IOBuffer to an
    // aggregated buffer.
    bool ConsumeBytesRead(URLRequest* request, int num_bytes);

    // Called once the job has exceeded its deadline.
    void OnTimeout();

    // Called when the URLRequest has completed (either success or failure).
    void OnUrlRequestCompleted(URLRequest* request);

    // Called when the Job has completed. The job may finish in response to a
    // timeout, an invalid URL, or the URLRequest completing. By the time this
    // method is called, the response variables have been assigned
    // (result_net_error_ and response_body_).
    void OnJobCompleted();

    // The requests attached to this job.
    RequestList requests_;

    // The input parameters for starting a URLRequest.
    std::unique_ptr<RequestParams> request_params_;

    // The URLRequest response information.
    std::vector<uint8_t> response_body_;
    Error result_net_error_;

    std::unique_ptr<URLRequest> url_request_;
    scoped_refptr<IOBuffer> read_buffer_;

    // Used to timeout the job when the URLRequest takes too long. This timer is
    // also used for notifying a failure to start the URLRequest.
    base::OneShotTimer timer_;

    // Non-owned pointer to the CertNetFetcherImpl that created this job.
    CertNetFetcherImpl* parent_;

    DISALLOW_COPY_AND_ASSIGN(Job);
};

CertNetFetcherImpl::RequestImpl::~RequestImpl()
{
    if (job_)
        job_->DetachRequest(this);
}

CertNetFetcherImpl::Job::Job(std::unique_ptr<RequestParams> request_params,
    CertNetFetcherImpl* parent)
    : request_params_(std::move(request_params))
    , result_net_error_(ERR_IO_PENDING)
    , parent_(parent)
{
}

CertNetFetcherImpl::Job::~Job()
{
    Cancel();
}

void CertNetFetcherImpl::Job::Cancel()
{
    parent_ = nullptr;

    // Notify each request of cancellation and remove it from the list.
    for (base::LinkNode<RequestImpl>* current = requests_.head();
         current != requests_.end();) {
        base::LinkNode<RequestImpl>* next = current->next();
        current->value()->OnJobCancelled(this);
        current->RemoveFromList();
        current = next;
    }

    DCHECK(requests_.empty());

    Stop();
}

std::unique_ptr<CertNetFetcher::Request> CertNetFetcherImpl::Job::CreateRequest(
    const FetchCallback& callback)
{
    std::unique_ptr<RequestImpl> request(new RequestImpl(this, callback));
    requests_.Append(request.get());
    return std::move(request);
}

void CertNetFetcherImpl::Job::DetachRequest(RequestImpl* request)
{
    std::unique_ptr<Job> delete_this;

    request->RemoveFromList();

    // If there are no longer any requests attached to the job then
    // cancel and delete it.
    if (requests_.empty() && !parent_->IsCurrentlyCompletingJob(this))
        delete_this = parent_->RemoveJob(this);
}

void CertNetFetcherImpl::Job::StartURLRequest(URLRequestContext* context)
{
    Error error = CanFetchUrl(request_params_->url);
    if (error != OK) {
        result_net_error_ = error;
        // The CertNetFetcher's API contract is that requests always complete
        // asynchronously. Use the timer class so the task is easily cancelled.
        timer_.Start(FROM_HERE, base::TimeDelta(), this, &Job::OnJobCompleted);
        return;
    }

    // Start the URLRequest.
    read_buffer_ = new IOBuffer(kReadBufferSizeInBytes);
    url_request_ = context->CreateRequest(request_params_->url, DEFAULT_PRIORITY, this);
    if (request_params_->http_method == HTTP_METHOD_POST)
        url_request_->set_method("POST");
    url_request_->SetLoadFlags(LOAD_DO_NOT_SAVE_COOKIES | LOAD_DO_NOT_SEND_COOKIES);
    url_request_->Start();

    // Start a timer to limit how long the job runs for.
    if (request_params_->timeout > base::TimeDelta())
        timer_.Start(FROM_HERE, request_params_->timeout, this, &Job::OnTimeout);
}

void CertNetFetcherImpl::Job::OnReceivedRedirect(
    URLRequest* request,
    const RedirectInfo& redirect_info,
    bool* defer_redirect)
{
    DCHECK_EQ(url_request_.get(), request);

    // Ensure that the new URL matches the policy.
    Error error = CanFetchUrl(redirect_info.new_url);
    if (error != OK) {
        request->CancelWithError(error);
        OnUrlRequestCompleted(request);
        return;
    }
}

void CertNetFetcherImpl::Job::OnResponseStarted(URLRequest* request)
{
    DCHECK_EQ(url_request_.get(), request);

    if (!request->status().is_success()) {
        OnUrlRequestCompleted(request);
        return;
    }

    if (request->GetResponseCode() != 200) {
        // TODO(eroman): Use a more specific error code.
        request->CancelWithError(ERR_FAILED);
        OnUrlRequestCompleted(request);
        return;
    }

    ReadBody(request);
}

void CertNetFetcherImpl::Job::OnReadCompleted(URLRequest* request,
    int bytes_read)
{
    DCHECK_EQ(url_request_.get(), request);

    // Keep reading the response body.
    if (ConsumeBytesRead(request, bytes_read))
        ReadBody(request);
}

void CertNetFetcherImpl::Job::Stop()
{
    timer_.Stop();
    url_request_.reset();
}

void CertNetFetcherImpl::Job::ReadBody(URLRequest* request)
{
    // Read as many bytes as are available synchronously.
    int num_bytes;
    while (
        request->Read(read_buffer_.get(), kReadBufferSizeInBytes, &num_bytes)) {
        if (!ConsumeBytesRead(request, num_bytes))
            return;
    }

    // Check whether the read failed synchronously.
    if (!request->status().is_io_pending())
        OnUrlRequestCompleted(request);
    return;
}

bool CertNetFetcherImpl::Job::ConsumeBytesRead(URLRequest* request,
    int num_bytes)
{
    if (num_bytes <= 0) {
        // Error while reading, or EOF.
        OnUrlRequestCompleted(request);
        return false;
    }

    // Enforce maximum size bound.
    if (num_bytes + response_body_.size() > request_params_->max_response_bytes) {
        request->CancelWithError(ERR_FILE_TOO_BIG);
        OnUrlRequestCompleted(request);
        return false;
    }

    // Append the data to |response_body_|.
    response_body_.reserve(num_bytes);
    response_body_.insert(response_body_.end(), read_buffer_->data(),
        read_buffer_->data() + num_bytes);
    return true;
}

void CertNetFetcherImpl::Job::OnTimeout()
{
    result_net_error_ = ERR_TIMED_OUT;
    url_request_->CancelWithError(result_net_error_);
    OnJobCompleted();
}

void CertNetFetcherImpl::Job::OnUrlRequestCompleted(URLRequest* request)
{
    DCHECK_EQ(request, url_request_.get());

    if (request->status().is_success())
        result_net_error_ = OK;
    else
        result_net_error_ = static_cast<Error>(request->status().error());

    OnJobCompleted();
}

void CertNetFetcherImpl::Job::OnJobCompleted()
{
    // Stop the timer and clear the URLRequest.
    Stop();

    // Invoking the callbacks is subtle as state may be mutated while iterating
    // through the callbacks:
    //
    //   * The parent CertNetFetcherImpl may be deleted
    //   * Requests in this job may be cancelled

    std::unique_ptr<Job> delete_this = parent_->RemoveJob(this);
    parent_->SetCurrentlyCompletingJob(this);

    while (!requests_.empty()) {
        base::LinkNode<RequestImpl>* request = requests_.head();
        request->RemoveFromList();
        request->value()->OnJobCompleted(this, result_net_error_, response_body_);
    }

    if (parent_)
        parent_->ClearCurrentlyCompletingJob(this);
}

CertNetFetcherImpl::CertNetFetcherImpl(URLRequestContext* context)
    : currently_completing_job_(nullptr)
    , context_(context)
{
}

CertNetFetcherImpl::~CertNetFetcherImpl()
{
    STLDeleteElements(&jobs_);

    // The CertNetFetcherImpl was destroyed in a FetchCallback. Detach all
    // remaining requests from the job so no further callbacks are called.
    if (currently_completing_job_)
        currently_completing_job_->Cancel();
}

std::unique_ptr<CertNetFetcher::Request> CertNetFetcherImpl::FetchCaIssuers(
    const GURL& url,
    int timeout_milliseconds,
    int max_response_bytes,
    const FetchCallback& callback)
{
    std::unique_ptr<RequestParams> request_params(new RequestParams);

    request_params->url = url;
    request_params->http_method = HTTP_METHOD_GET;
    request_params->timeout = GetTimeout(timeout_milliseconds);
    request_params->max_response_bytes = GetMaxResponseBytes(max_response_bytes, kMaxResponseSizeInBytesForAia);

    return Fetch(std::move(request_params), callback);
}

std::unique_ptr<CertNetFetcher::Request> CertNetFetcherImpl::FetchCrl(
    const GURL& url,
    int timeout_milliseconds,
    int max_response_bytes,
    const FetchCallback& callback)
{
    std::unique_ptr<RequestParams> request_params(new RequestParams);

    request_params->url = url;
    request_params->http_method = HTTP_METHOD_GET;
    request_params->timeout = GetTimeout(timeout_milliseconds);
    request_params->max_response_bytes = GetMaxResponseBytes(max_response_bytes, kMaxResponseSizeInBytesForCrl);

    return Fetch(std::move(request_params), callback);
}

std::unique_ptr<CertNetFetcher::Request> CertNetFetcherImpl::FetchOcsp(
    const GURL& url,
    int timeout_milliseconds,
    int max_response_bytes,
    const FetchCallback& callback)
{
    std::unique_ptr<RequestParams> request_params(new RequestParams);

    request_params->url = url;
    request_params->http_method = HTTP_METHOD_GET;
    request_params->timeout = GetTimeout(timeout_milliseconds);
    request_params->max_response_bytes = GetMaxResponseBytes(max_response_bytes, kMaxResponseSizeInBytesForAia);

    return Fetch(std::move(request_params), callback);
}

bool CertNetFetcherImpl::JobComparator::operator()(const Job* job1,
    const Job* job2) const
{
    return job1->request_params() < job2->request_params();
}

std::unique_ptr<CertNetFetcher::Request> CertNetFetcherImpl::Fetch(
    std::unique_ptr<RequestParams> request_params,
    const FetchCallback& callback)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    // If there is an in-progress job that matches the request parameters use it.
    // Otherwise start a new job.
    Job* job = FindJob(*request_params);

    if (!job) {
        job = new Job(std::move(request_params), this);
        jobs_.insert(job);
        job->StartURLRequest(context_);
    }

    return job->CreateRequest(callback);
}

struct CertNetFetcherImpl::JobToRequestParamsComparator {
    bool operator()(const Job* job,
        const CertNetFetcherImpl::RequestParams& value) const
    {
        return job->request_params() < value;
    }
};

CertNetFetcherImpl::Job* CertNetFetcherImpl::FindJob(
    const RequestParams& params)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    // The JobSet is kept in sorted order so items can be found using binary
    // search.
    JobSet::iterator it = std::lower_bound(jobs_.begin(), jobs_.end(), params,
        JobToRequestParamsComparator());
    if (it != jobs_.end() && !(params < (*it)->request_params()))
        return *it;
    return nullptr;
}

std::unique_ptr<CertNetFetcherImpl::Job> CertNetFetcherImpl::RemoveJob(
    Job* job)
{
    DCHECK(thread_checker_.CalledOnValidThread());
    bool erased_job = jobs_.erase(job) == 1;
    CHECK(erased_job);
    return base::WrapUnique(job);
}

void CertNetFetcherImpl::SetCurrentlyCompletingJob(Job* job)
{
    DCHECK(!currently_completing_job_);
    DCHECK(job);
    currently_completing_job_ = job;
}

void CertNetFetcherImpl::ClearCurrentlyCompletingJob(Job* job)
{
    DCHECK_EQ(currently_completing_job_, job);
    currently_completing_job_ = nullptr;
}

bool CertNetFetcherImpl::IsCurrentlyCompletingJob(Job* job)
{
    return job == currently_completing_job_;
}

} // namespace net
